define(function(require, exports, module) {
	return function(jQuery) {
		/**
		 * Autocomplete - jQuery plugin 1.0.2
		 * 
		 * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar,
		 * Jörn Zaefferer
		 * 
		 * Dual licensed under the MIT and GPL licenses:
		 * http://www.opensource.org/licenses/mit-license.php
		 * http://www.gnu.org/licenses/gpl.html
		 * 
		 * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z
		 * joern.zaefferer $
		 * 
		 */
(function($) {

			$.fn.extend({
						autocomplete : function(urlOrData, options) {
							var isUrl = typeof urlOrData == "string";
							options = $.extend({}, $.Autocompleter.defaults, {
										url : isUrl ? urlOrData : null,
										data : isUrl ? null : urlOrData,
										delay : isUrl
												? $.Autocompleter.defaults.delay
												: 10,
										max : options && !options.scroll
												? 10
												: 150
									}, options);

							// if highlight is set to false, replace it with a
							// do-nothing function
							options.highlight = options.highlight
									|| function(value) {
										return value;
									};

							// if the formatMatch option is not specified, then
							// use formatItem for
							// backwards compatibility
							options.formatMatch = options.formatMatch
									|| options.formatItem;

							return this.each(function() {
										new $.Autocompleter(this, options);
									});
						},
						result : function(handler) {
							return this.bind("result", handler);
						},
						search : function(handler) {
							return this.trigger("search", [handler]);
						},
						flushCache : function() {
							return this.trigger("flushCache");
						},
						setOptions : function(options) {
							return this.trigger("setOptions", [options]);
						},
						unautocomplete : function() {
							return this.trigger("unautocomplete");
						}
					});

			$.Autocompleter = function(input, options) {

				var KEY = {
					UP : 38,
					DOWN : 40,
					DEL : 46,
					TAB : 9,
					RETURN : 13,
					ESC : 27,
					COMMA : 188,
					PAGEUP : 33,
					PAGEDOWN : 34,
					BACKSPACE : 8
				};

				// Create $ object for input element
				var $input = $(input).attr("autocomplete", "off")
						.addClass(options.inputClass);

				var timeout;
				var previousValue = "";
				var cache = $.Autocompleter.Cache(options);
				var hasFocus = 0;
				var lastKeyPressCode;
				var config = {
					mouseDownOnSelect : false
				};
				var select = $.Autocompleter.Select(options, input,
						selectCurrent, config);

				var blockSubmit;

				// prevent form submit in opera when selecting with return key
				$.browser.opera
						&& $(input.form).bind("submit.autocomplete",
								function() {
									if (blockSubmit) {
										blockSubmit = false;
										return false;
									}
								});

				// only opera doesn't trigger keydown multiple times while
				// pressed, others
				// don't work with keypress at all
				$input.bind(
						($.browser.opera ? "keypress" : "keydown")
								+ ".autocomplete", function(event) {
							// track last key pressed
							lastKeyPressCode = event.keyCode;
							switch (event.keyCode) {

								case KEY.UP :
									event.preventDefault();
									if (select.visible()) {
										select.prev();
									} else {
										onChange(0, true);
									}
									break;

								case KEY.DOWN :
									event.preventDefault();
									if (select.visible()) {
										select.next();
									} else {
										onChange(0, true);
									}
									break;

								case KEY.PAGEUP :
									event.preventDefault();
									if (select.visible()) {
										select.pageUp();
									} else {
										onChange(0, true);
									}
									break;

								case KEY.PAGEDOWN :
									event.preventDefault();
									if (select.visible()) {
										select.pageDown();
									} else {
										onChange(0, true);
									}
									break;

								// matches also semicolon
								case options.multiple
										&& $.trim(options.multipleSeparator) == ","
										&& KEY.COMMA :
								case KEY.TAB :
								case KEY.RETURN :
									if (selectCurrent()) {
										// stop default to prevent a form
										// submit, Opera needs
										// special handling
										event.preventDefault();
										blockSubmit = true;
										return false;
									}
									break;

								case KEY.ESC :
									select.hide();
									break;

								default :
									clearTimeout(timeout);
									timeout = setTimeout(onChange,
											options.delay);
									break;
							}
						}).focus(function() {
							// track whether the field has focus, we shouldn't
							// process any
							// results if the field no longer has focus
							hasFocus++;
						}).blur(function() {
							hasFocus = 0;
							if (!config.mouseDownOnSelect) {
								hideResults();
							}
						}).click(function() {
							// show select when clicking in a focused field
							if (hasFocus++ > 1 && !select.visible()) {
								onChange(0, true);
							}
						}).bind("search", function() {
					// TODO why not just specifying both arguments?
					var fn = (arguments.length > 1) ? arguments[1] : null;
					function findValueCallback(q, data) {
						var result;
						if (data && data.length) {
							for (var i = 0; i < data.length; i++) {
								if (data[i].result.toLowerCase() == q
										.toLowerCase()) {
									result = data[i];
									break;
								}
							}
						}
						if (typeof fn == "function")
							fn(result);
						else
							$input.trigger("result", result
											&& [result.data, result.value]);
					}
					$.each(trimWords($input.val()), function(i, value) {
								request(value, findValueCallback,
										findValueCallback);
							});
				}).bind("flushCache", function() {
							cache.flush();
						}).bind("setOptions", function() {
							$.extend(options, arguments[1]);
							// if we've updated the data, repopulate
							if ("data" in arguments[1])
								cache.populate();
						})
						// ���ӣ��޸���FF�����Ĳ��������⣩
						/*
						 * .bind("input",function(){ onChange(0,true); })
						 */
						.bind("input", function() {
									onChange(0, true);
								}).bind("unautocomplete", function() {
							select.unbind();
							$input.unbind();
							// �޸�ǰ(�޸���FF�����Ĳ���������)
							// $(input.form).unbind(".autocomplete");
							// �޸ĺ�
							$(input.form).unbind(".autocomplete").bind("input",
									function() {
										onChange(0, true);
									});
						});

				function selectCurrent() {
					var selected = select.selected();
					if (!selected)
						return false;

					var v = selected.result;
					previousValue = v;

					if (options.multiple) {
						var words = trimWords($input.val());
						if (words.length > 1) {
							v = words.slice(0, words.length - 1)
									.join(options.multipleSeparator)
									+ options.multipleSeparator + v;
						}
						v += options.multipleSeparator;
					}

					$input.val(v);
					hideResultsNow();
					$input.trigger("result", [selected.data, selected.value]);
					return true;
				}

				function onChange(crap, skipPrevCheck) {
					if (lastKeyPressCode == KEY.DEL) {
						select.hide();
						return;
					}

					var currentValue = $input.val();

					if (!skipPrevCheck && currentValue == previousValue)
						return;

					previousValue = currentValue;

					currentValue = lastWord(currentValue);
					if (currentValue.length >= options.minChars) {
						$input.addClass(options.loadingClass);
						if (!options.matchCase)
							currentValue = currentValue.toLowerCase();
						request(currentValue, receiveData, hideResultsNow);
					} else {
						stopLoading();
						select.hide();
					}
				};

				function trimWords(value) {
					if (!value) {
						return [""];
					}
					var words = value.split(options.multipleSeparator);
					var result = [];
					$.each(words, function(i, value) {
								if ($.trim(value))
									result[i] = $.trim(value);
							});
					return result;
				}

				function lastWord(value) {
					if (!options.multiple)
						return value;
					var words = trimWords(value);
					return words[words.length - 1];
				}

				// fills in the input box w/the first match (assumed to be the
				// best match)
				// q: the term entered
				// sValue: the first matching result
				function autoFill(q, sValue) {
					// autofill in the complete box w/the first match as long as
					// the user
					// hasn't entered in more data
					// if the last user key pressed was backspace, don't
					// autofill
					if (options.autoFill
							&& (lastWord($input.val()).toLowerCase() == q
									.toLowerCase())
							&& lastKeyPressCode != KEY.BACKSPACE) {
						// fill in the value (keep the case the user has typed)
						$input
								.val($input.val()
										+ sValue
												.substring(lastWord(previousValue).length));
						// select the portion of the value not typed by the user
						// (so the
						// next character will erase)
						$.Autocompleter.Selection(input, previousValue.length,
								previousValue.length + sValue.length);
					}
				};

				function hideResults() {
					clearTimeout(timeout);
					timeout = setTimeout(hideResultsNow, 200);
				};

				function hideResultsNow() {
					var wasVisible = select.visible();
					select.hide();
					clearTimeout(timeout);
					stopLoading();
					if (options.mustMatch) {
						// call search and run callback
						$input.search(function(result) {
									// if no value found, clear the input box
									if (!result) {
										if (options.multiple) {
											var words = trimWords($input.val())
													.slice(0, -1);
											$input
													.val(words
															.join(options.multipleSeparator)
															+ (words.length
																	? options.multipleSeparator
																	: ""));
										} else
											$input.val("");
									}
								});
					}
					if (wasVisible)
						// position cursor at end of input field
						$.Autocompleter.Selection(input, input.value.length,
								input.value.length);
				};

				function receiveData(q, data) {
					if (data && data.length && hasFocus) {
						stopLoading();
						select.display(data, q);
						autoFill(q, data[0].value);
						select.show();
					} else {
						hideResultsNow();
					}
				};

				function request(term, success, failure) {
					if (!options.matchCase)
						term = term.toLowerCase();
					var data = cache.load(term);
					// recieve the cached data
					if (data && data.length) {
						success(term, data);
						// if an AJAX url has been supplied, try loading the
						// data now
					} else if ((typeof options.url == "string")
							&& (options.url.length > 0)) {

						var extraParams = {
							timestamp : +new Date()
						};
						$.each(options.extraParams, function(key, param) {
									extraParams[key] = typeof param == "function"
											? param()
											: param;
								});

						$.ajax({
									// try to leverage ajaxQueue plugin to abort
									// previous requests
									mode : "abort",
									// limit abortion to this input
									port : "autocomplete" + input.name,
									dataType : options.dataType,
									url : options.url,
									data : $.extend({
												q : lastWord(term),
												limit : options.max
											}, extraParams),
									success : function(data) {
										if (data.detail == null) {
											var parsed = options.parse
													&& options.parse(data)
													|| parse(data);
											cache.add(term, parsed);
											success(term, parsed);
										} else {
											stopLoading();
											$.syse(data.detail);
										}
									}
								});
					} else {
						// if we have a failure, we need to empty the list --
						// this prevents
						// the the [TAB] key from selecting the last successful
						// match
						select.emptyList();
						failure(term);
					}
				};

				function parse(data) {
					var parsed = [];
					var rows = data.split("\n");
					for (var i = 0; i < rows.length; i++) {
						var row = $.trim(rows[i]);
						if (row) {
							row = row.split("|");
							parsed[parsed.length] = {
								data : row,
								value : row[0],
								result : options.formatResult
										&& options.formatResult(row, row[0])
										|| row[0]
							};
						}
					}
					return parsed;
				};

				function stopLoading() {
					$input.removeClass(options.loadingClass);
				};

			};

			$.Autocompleter.defaults = {
				inputClass : "ac_input",
				resultsClass : "ac_results",
				loadingClass : "ac_loading",
				minChars : 1,
				delay : 400,
				matchCase : false,
				matchSubset : true,
				matchContains : false,
				cacheLength : 10,
				max : 100,
				mustMatch : false,
				extraParams : {},
				selectFirst : true,
				formatItem : function(row) {
					return row[0];
				},
				formatMatch : null,
				autoFill : false,
				width : 0,
				multiple : false,
				multipleSeparator : ", ",
				highlight : function(value, term) {
					return value
							.replace(
									new RegExp(
											"(?![^&;]+;)(?!<[^<>]*)("
													+ term
															.replace(
																	/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi,
																	"\\$1")
													+ ")(?![^<>]*>)(?![^&;]+;)",
											"gi"), "<strong>$1</strong>");
				},
				scroll : true,
				scrollHeight : 180
			};

			$.Autocompleter.Cache = function(options) {

				var data = {};
				var length = 0;

				function matchSubset(s, sub) {
					if (!options.matchCase)
						s = s.toLowerCase();
					var i = s.indexOf(sub);
					if (i == -1)
						return false;
					return i == 0 || options.matchContains;
				};

				function add(q, value) {
					if (length > options.cacheLength) {
						flush();
					}
					if (!data[q]) {
						length++;
					}
					data[q] = value;
				}

				function populate() {
					if (!options.data)
						return false;
					// track the matches
					var stMatchSets = {}, nullData = 0;

					// no url was specified, we need to adjust the cache length
					// to make sure
					// it fits the local data store
					if (!options.url)
						options.cacheLength = 1;

					// track all options for minChars = 0
					stMatchSets[""] = [];

					// loop through the array and create a lookup structure
					for (var i = 0, ol = options.data.length; i < ol; i++) {
						var rawValue = options.data[i];
						// if rawValue is a string, make an array otherwise just
						// reference
						// the array
						rawValue = (typeof rawValue == "string")
								? [rawValue]
								: rawValue;

						var value = options.formatMatch(rawValue, i + 1,
								options.data.length);
						if (value === false)
							continue;

						var firstChar = value.charAt(0).toLowerCase();
						// if no lookup array for this character exists, look it
						// up now
						if (!stMatchSets[firstChar])
							stMatchSets[firstChar] = [];

						// if the match is a string
						var row = {
							value : value,
							data : rawValue,
							result : options.formatResult
									&& options.formatResult(rawValue) || value
						};

						// push the current match into the set list
						stMatchSets[firstChar].push(row);

						// keep track of minChars zero items
						if (nullData++ < options.max) {
							stMatchSets[""].push(row);
						}
					};

					// add the data items to the cache
					$.each(stMatchSets, function(i, value) {
								// increase the cache size
								options.cacheLength++;
								// add to the cache
								add(i, value);
							});
				}

				// populate any existing data
				setTimeout(populate, 25);

				function flush() {
					data = {};
					length = 0;
				}

				return {
					flush : flush,
					add : add,
					populate : populate,
					load : function(q) {
						if (!options.cacheLength || !length)
							return null;
						/**
						 * if dealing w/local data and matchContains than we
						 * must make sure to loop through all the data
						 * collections looking for matches
						 */
						if (!options.url && options.matchContains) {
							// track all matches
							var csub = [];
							// loop through all the data grids for matches
							for (var k in data) {
								// don't search through the stMatchSets[""]
								// (minChars: 0)
								// cache
								// this prevents duplicates
								if (k.length > 0) {
									var c = data[k];
									$.each(c, function(i, x) {
												// if we've got a match, add it
												// to the array
												if (matchSubset(x.value, q)) {
													csub.push(x);
												}
											});
								}
							}
							return csub;
						} else
						// if the exact item exists, use it
						if (data[q]) {
							return data[q];
						} else if (options.matchSubset) {
							for (var i = q.length - 1; i >= options.minChars; i--) {
								var c = data[q.substr(0, i)];
								if (c) {
									var csub = [];
									$.each(c, function(i, x) {
												if (matchSubset(x.value, q)) {
													csub[csub.length] = x;
												}
											});
									return csub;
								}
							}
						}
						return null;
					}
				};
			};

			$.Autocompleter.Select = function(options, input, select, config) {
				var CLASSES = {
					ACTIVE : "ac_over"
				};

				var listItems, active = -1, data, term = "", needsInit = true, element, list;

				// Create results
				function init() {
					if (!needsInit)
						return;
					element = $("<div/>").hide().addClass(options.resultsClass)
							.css("position", "absolute")
							.appendTo(document.body);

					list = $("<ul/>").appendTo(element).mouseover(
							function(event) {
								if (target(event).nodeName
										&& target(event).nodeName.toUpperCase() == 'LI') {
									active = $("li", list)
											.removeClass(CLASSES.ACTIVE)
											.index(target(event));
									$(target(event)).addClass(CLASSES.ACTIVE);
								}
							}).click(function(event) {
								$(target(event)).addClass(CLASSES.ACTIVE);
								select();
								// TODO provide option to avoid setting focus
								// again after selection?
								// useful for cleanup-on-focus
								input.focus();
								return false;
							}).mousedown(function() {
								config.mouseDownOnSelect = true;
							}).mouseup(function() {
								config.mouseDownOnSelect = false;
							});

					if (options.width > 0)
						element.css("width", options.width);

					needsInit = false;
				}

				function target(event) {
					var element = event.target;
					while (element && element.tagName != "LI")
						element = element.parentNode;
					// more fun with IE, sometimes event.target is empty, just
					// ignore it
					// then
					if (!element)
						return [];
					return element;
				}

				function moveSelect(step) {
					listItems.slice(active, active + 1)
							.removeClass(CLASSES.ACTIVE);
					movePosition(step);
					var activeItem = listItems.slice(active, active + 1)
							.addClass(CLASSES.ACTIVE);
					if (options.scroll) {
						var offset = 0;
						listItems.slice(0, active).each(function() {
									offset += this.offsetHeight;
								});
						if ((offset + activeItem[0].offsetHeight - list
								.scrollTop()) > list[0].clientHeight) {
							list.scrollTop(offset + activeItem[0].offsetHeight
									- list.innerHeight());
						} else if (offset < list.scrollTop()) {
							list.scrollTop(offset);
						}
					}
				};

				function movePosition(step) {
					active += step;
					if (active < 0) {
						active = listItems.size() - 1;
					} else if (active >= listItems.size()) {
						active = 0;
					}
				}

				function limitNumberOfItems(available) {
					return options.max && options.max < available
							? options.max
							: available;
				}

				function fillList() {
					list.empty();
					var max = limitNumberOfItems(data.length);
					for (var i = 0; i < max; i++) {
						if (!data[i])
							continue;
						var formatted = options.formatItem(data[i].data, i + 1,
								max, data[i].value, term);
						if (formatted === false)
							continue;
						var li = $("<li/>").html(options.highlight(formatted,
								term)).addClass(i % 2 == 0
								? "ac_even"
								: "ac_odd").appendTo(list)[0];
						$.data(li, "ac_data", data[i]);
					}
					listItems = list.find("li");
					if (options.selectFirst) {
						listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
						active = 0;
					}
					// apply bgiframe if available
					if ($.fn.bgiframe)
						list.bgiframe();
				}

				return {
					display : function(d, q) {
						init();
						data = d;
						term = q;
						fillList();
					},
					next : function() {
						moveSelect(1);
					},
					prev : function() {
						moveSelect(-1);
					},
					pageUp : function() {
						if (active != 0 && active - 8 < 0) {
							moveSelect(-active);
						} else {
							moveSelect(-8);
						}
					},
					pageDown : function() {
						if (active != listItems.size() - 1
								&& active + 8 > listItems.size()) {
							moveSelect(listItems.size() - 1 - active);
						} else {
							moveSelect(8);
						}
					},
					hide : function() {
						element && element.hide();
						listItems && listItems.removeClass(CLASSES.ACTIVE);
						active = -1;
					},
					visible : function() {
						return element && element.is(":visible");
					},
					current : function() {
						return this.visible()
								&& (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst
										&& listItems[0]);
					},
					show : function() {
						var offset = $(input).offset();
						element.css({
							width : typeof options.width == "string"
									|| options.width > 0
									? options.width
									: $(input).width(),
							top : offset.top + input.offsetHeight,
							left : offset.left
						}).show();
						if (options.scroll) {
							list.scrollTop(0);
							list.css({
										maxHeight : options.scrollHeight,
										overflow : 'auto'
									});

							if ($.browser.msie
									&& typeof document.body.style.maxHeight === "undefined") {
								var listHeight = 0;
								listItems.each(function() {
											listHeight += this.offsetHeight;
										});
								var scrollbarsVisible = listHeight > options.scrollHeight;
								list.css('height', scrollbarsVisible
												? options.scrollHeight
												: listHeight);
								if (!scrollbarsVisible) {
									// IE doesn't recalculate width when
									// scrollbar
									// disappears
									listItems.width(list.width()
											- parseInt(listItems
													.css("padding-left"))
											- parseInt(listItems
													.css("padding-right")));
								}
							}

						}
					},
					selected : function() {
						var selected = listItems
								&& listItems.filter("." + CLASSES.ACTIVE)
										.removeClass(CLASSES.ACTIVE);
						return selected && selected.length
								&& $.data(selected[0], "ac_data");
					},
					emptyList : function() {
						list && list.empty();
					},
					unbind : function() {
						element && element.remove();
					}
				};
			};

			$.Autocompleter.Selection = function(field, start, end) {
				if (field.createTextRange) {
					var selRange = field.createTextRange();
					selRange.collapse(true);
					selRange.moveStart("character", start);
					selRange.moveEnd("character", end);
					selRange.select();
				} else if (field.setSelectionRange) {
					field.setSelectionRange(start, end);
				} else {
					if (field.selectionStart) {
						field.selectionStart = start;
						field.selectionEnd = end;
					}
				}
				field.focus();
			};

		})(jQuery);
		
		return jQuery.noConflict(true);
	}
});